package br.com.sanambiental.san.dao;

import java.io.Serializable;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import javax.annotation.Resource;

import org.hibernate.Criteria;
import org.hibernate.Query;
import org.hibernate.ScrollableResults;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Example;
import org.hibernate.criterion.MatchMode;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projection;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.hibernate.criterion.SimpleExpression;
import org.hibernate.impl.CriteriaImpl;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.transform.ResultTransformer;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;

import br.gov.caixa.pedesgo.arquitetura.comum.fabrica.ColecaoFactory;
import br.gov.caixa.pedesgo.arquitetura.comum.fabrica.NumeroFactory;
import br.gov.caixa.pedesgo.arquitetura.comum.objeto.UtilObjeto;
import br.gov.caixa.pedesgo.arquitetura.comum.string.UtilString;
import br.gov.caixa.pedesgo.arquitetura.entidade.Entidade;
import br.gov.caixa.pedesgo.arquitetura.integracao.DAO;
import br.gov.caixa.pedesgo.arquitetura.reflexao.UtilReflexaoGeneric;

/**
 * Classe respons�vel pela persist�ncia usando hibernate.
 * 
 * @param <T>
 *            Tipo da entidade
 */
public abstract class HibernateDAOAbstrato<T extends Entidade> extends
	HibernateDaoSupport {

    /**
     * @see DAO#alterar(Entidade)
     */
    public void alterar(T entidade) {
	if (isReferencia(entidade)) {
	    getHibernateTemplate().update(entidade);
	}
    }

    /**
     * @see DAO#consultar()
     */
    public Collection<T> consultar() {
	Criteria criteria = novoCriteria();
	return consultar(criteria);
    }

    /**
     * @see DAO#consultar(Entidade)
     */
    public Collection<T> consultar(T entidade) {
	Criteria criteria = novoCriteria();

	if (isReferencia(entidade)) {
	    Example example = Example.create(entidade);
	    example.enableLike(MatchMode.START);
	    example.excludeZeroes();
	    criteria.add(example);
	}

	return consultar(criteria);
    }

    /**
     * @see DAO#inserir(Entidade)
     */
    public Serializable inserir(T entidade) {
	Serializable resultado = null;

	if (isReferencia(entidade)) {
	    resultado = getHibernateTemplate().save(entidade);
	}
	return resultado;
    }

    /**
     * @see DAO#obter(Serializable)
     */
    public T obter(Serializable pk) {
	T resultado = null;

	if (isReferencia(pk)) {
	    Class<T> tipo = getTipoDaEntidade();
	    resultado = (T) getHibernateTemplate().get(tipo, pk);
	}
	return resultado;
    }

    /**
     * @see DAO#remover(Entidade)
     */
    public void remover(T entidade) {
	if (isReferencia(entidade)) {
	    carregarEntidadePersistente(entidade);

	    getHibernateTemplate().delete(entidade);
	    getHibernateTemplate().flush();
	}
    }

    /**
     * @see DAO#removerTodos(Collection)
     */
    public void removerTodos(Collection<T> entidades) {
	if (isReferencia(entidades)) {
	    for (T entidade : entidades) {
		remover(entidade);
	    }
	}
    }

    /**
     * @see DAO#salvar(Entidade)
     */
    public void salvar(T entidade) {
	if (isReferencia(entidade)) {
	    getHibernateTemplate().saveOrUpdate(entidade);
	}
    }

    /**
     * Configura o SessionFactory do hibernate.
     * 
     * @param sessionFactory
     *            SessionFactory do hibernate
     */
    @Resource(name = "sessionFactory")
    protected void bindSessionFactory(SessionFactory sessionFactory) {
	setSessionFactory(sessionFactory);
    }

    /**
     * Efetua o load da entidade caso esta n�o seja uma entidade persistente.
     * 
     * @param entidade
     *            Entidade que ser� carregada como persistente.
     */
    protected void carregarEntidadePersistente(T entidade) {
	if (isReferencia(entidade) && !isPersistente(entidade)) {
	    Serializable pk = entidade.getIdentificador();
	    getHibernateTemplate().load(entidade, pk);
	}
    }

    /**
     * Retorna true se a entidade for persistente.
     * 
     * @param entidade
     *            Entidade validada.
     * @return true se a entidade for persistente.
     */
    protected boolean isPersistente(T entidade) {
	return (getSession().contains(entidade));
    }

    /**
     * Efetua a consulta de um criteria.
     * 
     * @param criteria
     *            Crit�ria que ser� executada.
     * @return Cole��o de entidades.
     */
    @SuppressWarnings("unchecked")
    protected Collection<T> consultar(Criteria criteria) {
	Collection<T> colecao = criteria.list();
	return colecao;
    }

    /**
     * Efetua a consulta de uma query.
     * 
     * @param query
     *            Query que ser� executada.
     * @return Cole��o de entidades.
     */
    @SuppressWarnings("unchecked")
    protected Collection<T> consultar(Query query) {
	Collection<T> colecao = query.list();
	return colecao;
    }

    /**
     * Efetua a consulta de um hql.
     * 
     * @param hql
     *            HQL que ser� executado.
     * @return Cole��o de entidades.
     */
    protected Collection<T> consultar(String hql) {
	Query query = novaQuery(hql);
	return consultar(query);
    }

    /**
     * Executa um update ou delete na query passada por par�metro.
     * 
     * @param query
     *            Query
     * @return quantidade de registros afetados.
     */
    protected int executar(Query query) {
	int resultado = 0;

	if (isReferencia(query)) {
	    resultado = query.executeUpdate();
	}
	return resultado;
    }

    /**
     * Obtem a entidade solicitada.
     * 
     * @param entidade
     *            Entidade
     * @return entidade
     */
    protected T obter(T entidade) {
	T resultado = null;

	if (isReferencia(entidade)) {
	    resultado = obter(entidade.getIdentificador());
	}
	return resultado;
    }

    /**
     * Consulta uma entidade da query passado por par�metro.
     * 
     * @param query
     *            Query que ser� executado.
     * @return Entidade
     */
    @SuppressWarnings("unchecked")
    protected T obter(Query query) {
	T resultado = (T) query.uniqueResult();
	return resultado;
    }

    /**
     * Retorna uma alias da consulta atual, caso o alias n�o exista ele ser�
     * criado.
     * 
     * @param query
     *            Query
     * @param alias
     *            Alias
     * @return alias da consulta atual
     */
    protected Criteria getAlias(Criteria query, String alias) {
	Criteria resultado = recuperarSubCriteria(query, alias);
	if (!isReferencia(resultado)) {
	    resultado = query.createAlias(alias, alias);
	}
	return resultado;
    }

    /**
     * Retorna o class metadata da entidade.
     * 
     * @return class metadata da entidade.
     */
    protected ClassMetadata getClassMetadata() {
	Class<?> classe = getTipoDaEntidade();
	return getSessionFactory().getClassMetadata(classe);
    }

    /**
     * @return f�brica de cole��o.
     */
    protected ColecaoFactory getColecaoFactory() {
	return ColecaoFactory.getInstancia();
    }

    /**
     * Retorna uma sub criteria da consulta atual, caso a sub criteira n�o
     * exista ela ser� criada.
     * 
     * @param query
     *            Query
     * @param alias
     *            Alias
     * @return sub criteria da consulta atual
     */
    protected Criteria getCriteria(Criteria query, String alias) {
	Criteria resultado = recuperarSubCriteria(query, alias);
	if (!isReferencia(resultado)) {
	    resultado = query.createCriteria(alias, alias);
	}
	return resultado;
    }

    /**
     * @return f�brica de n�mero.
     */
    protected NumeroFactory getNumeroFactory() {
	return NumeroFactory.getInstancia();
    }

    /**
     * Retorna a quantidade total de registros da consulta.
     * 
     * @param criteria
     *            Crit�rio da consulta.
     * @return quantidade total de registros da consulta.
     */
    @SuppressWarnings({ "boxing", "unchecked" })
    protected Integer getQuantidadeTotalDeRegistros(Criteria criteria) {
	Integer resultado = getNumeroFactory().novoInteger(0);
	if (isReferencia(criteria)) {
	    ResultTransformer rt = null;
	    if (UtilObjeto.isObjetoDoTipo(criteria, CriteriaImpl.class)) {

		rt = ((CriteriaImpl) criteria).getResultTransformer();
		Projection pj = ((CriteriaImpl) criteria).getProjection();

		// removendo os orders para evitar erro na consulta com count
		List<CriteriaImpl.OrderEntry> orders = getColecaoFactory()
			.novoArrayList();
		Iterator<CriteriaImpl.OrderEntry> iterator = ((CriteriaImpl) criteria)
			.iterateOrderings();
		while (iterator.hasNext()) {
		    orders.add(iterator.next());
		    iterator.remove();
		}

		criteria.setProjection(Projections.rowCount());
		resultado = (Integer) criteria.uniqueResult();
		criteria.setProjection(pj);

		// adicionando os orders no criteria novamente.
		for (CriteriaImpl.OrderEntry order : orders) {
		    order.getCriteria().addOrder(order.getOrder());
		}
		if (isReferencia(rt)) {
		    criteria.setResultTransformer(rt);
		}
	    } else {
		resultado = getQuantidadeTotalDeRegistros(criteria.scroll());
	    }
	}
	return resultado;
    }

    /**
     * Retorna a quantidade total de registros da consulta.
     * 
     * @param scrollable
     *            Crit�rio da consulta.
     * @return quantidade total de registros da consulta.
     */
    protected Integer getQuantidadeTotalDeRegistros(ScrollableResults scrollable) {
	Integer resultado = getNumeroFactory().novoInteger(0);
	if (isReferencia(scrollable)) {
	    scrollable.last();

	    int rowNumber = scrollable.getRowNumber();
	    resultado = getNumeroFactory().novoInteger(rowNumber + 1);
	}
	return resultado;
    }

    /**
     * Retorna nova query.
     * 
     * @param identificador
     *            Identificador do HQL mapeado no arquivo XML.
     * @return nova query.
     */
    protected Query getQuery(String identificador) {
	return getSession().getNamedQuery(identificador);
    }

    /**
     * Retorna o tipo da entidade. O tipo � recuperado a partir do generics.
     * 
     * @return Classe da entidade.
     */
    @SuppressWarnings("unchecked")
    protected Class<T> getTipoDaEntidade() {
	return (Class<T>) UtilReflexaoGeneric.getClasseDoTipo(this);
    }

    /**
     * @param objetos
     *            Objetos que ser�o validados.
     * @return true se o objeto tiver refer�ncia.
     * @see UtilObjeto#isReferencia(Object)
     */
    protected boolean isReferencia(Object... objetos) {
	return UtilObjeto.isReferenciaTodos(objetos);
    }

    /**
     * @param strings
     *            Strings que ser�o validadas.
     * @return true se a string for vazia.
     * @see UtilString#isVazio(String)
     */
    protected boolean isVazio(String... strings) {
	return UtilString.isVazioTodos(strings);
    }

    /**
     * Retorna true se a cole��o estiver vazia.
     * 
     * @param colecao
     *            Cole��o validada
     * @return true se a cole��o estiver vazia.
     */
    protected boolean isVazio(Collection<?> colecao) {
	return UtilObjeto.isVazio(colecao);
    }

    /**
     * Retorna true se o valor passado for zero ou nulo.
     * 
     * @param valor
     *            Valor que ser� validado
     * @return true se o valor passado for zero ou nulo.
     */
    @SuppressWarnings("boxing")
    protected boolean isZero(Number valor) {

	return !isReferencia(valor) || (valor.intValue() == 0);
    }

    /**
     * Retorna nova ordena��o ASC.
     * 
     * @param propriedade
     *            propriedade que ser� ordenada.
     * @return ordena��o ASC
     */
    protected Order novaOrdenacaoASC(String propriedade) {
	return Order.asc(propriedade);
    }

    /**
     * Retorna nova ordena��o DESC.
     * 
     * @param propriedade
     *            propriedade que ser� ordenada.
     * @return ordena��o DESC
     */
    protected Order novaOrdenacaoDESC(String propriedade) {
	return Order.desc(propriedade);
    }

    /**
     * Retorna nova query.
     * 
     * @param hql
     *            HQL
     * @return nova query.
     */
    protected Query novaQuery(String hql) {
	return getSession().createQuery(hql);
    }

    /**
     * Retorna novo criteria para entidade.
     * 
     * @return novo criteria para entidade.
     */
    protected Criteria novoCriteria() {
	Class<T> tipo = getTipoDaEntidade();
	return getSession().createCriteria(tipo);
    }

    /**
     * Retorna novo crit�rio.
     * 
     * @param propriedade
     *            Propriedade
     * @param arg0
     *            Argumento
     * @param arg1
     *            Argumento
     * @return crit�rio
     * @see Restrictions#between(String, Object, Object)
     */
    protected Criterion novoCriterioBetween(String propriedade, Object arg0,
	    Object arg1) {
	return Restrictions.between(propriedade, arg0, arg1);
    }

    /**
     * Retorna novo crit�rio.
     * 
     * @param propriedade
     *            Propriedade
     * @param arg0
     *            Argumento
     * @return crit�rio
     * @see Restrictions#eq(String, Object)
     */
    protected Criterion novoCriterioEQ(String propriedade, Object arg0) {
	return Restrictions.eq(propriedade, arg0);
    }

    /**
     * Retorna novo crit�rio.
     * 
     * @param propriedade
     *            Propriedade
     * @param arg0
     *            Argumento
     * @return crit�rio
     * @see Restrictions#eq(String, Object)
     */
    protected Criterion novoCriterioEQIgnoreCase(String propriedade, Object arg0) {
	SimpleExpression se = Restrictions.eq(propriedade, arg0);
	se.ignoreCase();
	return se;
    }

    /**
     * Retorna novo crit�rio.
     * 
     * @param propriedade
     *            Propriedade
     * @param arg0
     *            Argumento
     * @return crit�rio
     * @see Restrictions#like(String, Object)
     */
    protected Criterion novoCriterioLike(String propriedade, String arg0) {
	SimpleExpression se = null;
	se = Restrictions.like(propriedade, arg0, MatchMode.ANYWHERE);
	se.ignoreCase();
	return se;
    }

    /**
     * Retorna novo crit�rio.
     * 
     * @param propriedade
     *            Propriedade
     * @param arg0
     *            Argumento
     * @return crit�rio
     * @see Restrictions#like(String, Object)
     */
    protected Criterion novoCriterioLikeDireita(String propriedade, String arg0) {
	SimpleExpression se = null;
	se = Restrictions.like(propriedade, arg0, MatchMode.START);
	se.ignoreCase();
	return se;
    }

    /**
     * Retorna novo crit�rio.
     * 
     * @param propriedade
     *            Propriedade
     * @param arg0
     *            Argumento
     * @return crit�rio
     * @see Restrictions#ne(String, Object)
     */
    protected Criterion novoCriterioNE(String propriedade, Object arg0) {
	return Restrictions.ne(propriedade, arg0);
    }

    /**
     * Retorna o maior identificador da entidade. Ser� feita uma consulta do
     * tipo 'select max' para obter o maior ID da entidade. � necess�rio que a
     * entidade possuia um atributo identificador.
     * 
     * @param <T>
     *            Tipo do objeto que ser� retornado. Pode ser Integer ou Long,
     *            dependendo do tipo da entidade.
     * @return maior identificador da entidade.
     */
    @SuppressWarnings({ "hiding", "unchecked" })
    protected <T extends Number> T obterMaiorIdentificar() {
	ClassMetadata cm = getClassMetadata();
	String propriedadePK = cm.getIdentifierPropertyName();

	Criteria criteria = novoCriteria();
	criteria.setProjection(Projections.max(propriedadePK));
	return (T) criteria.uniqueResult();
    }

    /**
     * Remove a entidade pela chave prim�ria.
     * 
     * @param pk
     *            Chave prim�ria da entidade.
     */
    protected void remover(Serializable pk) {

	if (isReferencia(pk)) {
	    T entidade = obter(pk);
	    remover(entidade);
	}
    }

    /**
     * Retorna o criterio principal do criterio informado, caso o criterio
     * informado seja uma subcriteria, ser� recuperado o criterio pai at� chegar
     * ao criterio principal
     * 
     * @param query
     *            Criterio
     * @return criterio principal.
     */
    private Criteria getCriteriaPrincipal(Criteria query) {
	Criteria resultado = query;
	Class<?> subcriteria = CriteriaImpl.Subcriteria.class;

	while (UtilObjeto.isObjetoDoTipo(resultado, subcriteria)) {
	    resultado = ((CriteriaImpl.Subcriteria) resultado).getParent();
	}
	return resultado;
    }

    /**
     * Recupera uma sub criteria da query passada por par�metro.
     * 
     * @param query
     *            Query
     * @param alias
     *            Alias
     * @return sub criteria da query passada por par�metro.
     */
    @SuppressWarnings("unchecked")
    private Criteria recuperarSubCriteria(Criteria query, String alias) {
	Criteria resultado = null;
	Criteria principal = getCriteriaPrincipal(query);

	Iterator<Criteria> i = ((CriteriaImpl) principal).iterateSubcriteria();
	while (i.hasNext() && !isReferencia(resultado)) {
	    Criteria criteria = i.next();
	    if (UtilString.isStringsIguais(criteria.getAlias(), alias)) {
		resultado = criteria;
	    }
	}
	return resultado;
    }

}
